Máster en Data Science UAH

Tasador de viviendas de alquiler vacacional en Barcelona

Notebook #1 - Análisis exploratorio

Alumno: Héctor Mateos Oblanca
Tutor: Daniel Rodríguez Pérez

Contenidos

Limpieza y preparación de datos

In [1]:
import math
import json
import pandas as pd
import pandas_profiling
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import chart_studio.plotly as py
import plotly.graph_objs as go
import plotly.figure_factory as ff
from plotly.colors import n_colors
from plotly.offline import iplot, init_notebook_mode
from plotly.subplots import make_subplots

init_notebook_mode(connected=True)

%run src/utils.py

Carga de datos

Dataset principal Airbnb

In [2]:
city = 'barcelona'
month = '201909'
filename_in = 'src/data/' + city + '-' + month + '-listings.csv'
filename_out = 'src/data/' + city + '-' + month + '-listings-CLEAN.csv'

df = pd.read_csv(filename_in, low_memory=False)
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20404 entries, 0 to 20403
Columns: 106 entries, id to reviews_per_month
dtypes: float64(23), int64(21), object(62)
memory usage: 16.5+ MB

Dataset de evolución precios de alquiler de Idealista

In [3]:
dfi = pd.read_csv('src/geo/' + city + '.idealista-precio-alquiler-distritos.csv', sep=';')
dfi.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 11 entries, 0 to 10
Data columns (total 7 columns):
Localización            11 non-null object
Precio m2 oct 2019      11 non-null object
Variación mensual       11 non-null object
Variación trimestral    11 non-null object
Variación anual         11 non-null object
Máximo histórico        11 non-null object
Variación máximo        11 non-null object
dtypes: object(7)
memory usage: 744.0+ bytes

Carga de mapas

In [4]:
with open('src/geo/' + city + '.neighbourhoods.geojson') as f:
    city_nb = fix_geojson(json.load(f))

Descarte inicial de características

En el descarte inicial se desechan las características que se consideran menos útiles para el análisis exploratorio. Tras el análisis exploratorio habrá otro descarte de características que si bien habían sido interesantes para el exploratorio no lo serán para el modelado de la solución.

In [5]:
useless_cols = [
    'id',
    'listing_url',
    'scrape_id',
    'last_scraped',
    'name',
    'summary',
    'space',
    'description',
    'experiences_offered',
    'neighborhood_overview',
    'notes',
    'transit',
    'access',
    'interaction',
    'house_rules',
    'thumbnail_url',
    'medium_url',
    'picture_url',
    'xl_picture_url',
    'host_id',
    'host_url',
    'host_name',
    'host_since',
    'host_location',
    'host_about',
    'host_response_rate',
    'host_acceptance_rate',
    'host_is_superhost',
    'host_thumbnail_url',
    'host_picture_url',
    'host_neighbourhood',
    'host_listings_count',
    'host_total_listings_count',
    'host_has_profile_pic',
    'host_identity_verified',
    'host_verifications',
    'street',
    'neighbourhood',
    'city',
    'state',
    'zipcode',
    'market',
    'smart_location',
    'country_code',
    'country',
    'is_location_exact',
    'square_feet',
    'weekly_price',
    'monthly_price',
    'beds',
    'bed_type',
    'minimum_nights',
    'maximum_nights',
    'minimum_minimum_nights',
    'maximum_minimum_nights',
    'minimum_maximum_nights',
    'maximum_maximum_nights',
    'calendar_updated',
    'has_availability',
    'availability_30',
    'availability_60',
    'availability_90',
    'availability_365',
    'calendar_last_scraped',
    'requires_license',
    'jurisdiction_names',
    'is_business_travel_ready',
    'require_guest_profile_picture',
    'require_guest_phone_verification',
    'calculated_host_listings_count',
    'calculated_host_listings_count_entire_homes',
    'calculated_host_listings_count_private_rooms',
    'calculated_host_listings_count_shared_rooms'
]

df.drop(useless_cols, axis=1, inplace=True)

Armonización de barrios y distritos

Se unifican los nombres de los distritos de los distintos datasets para permitir join entre varios datasets cuando sea necesario. Para simplificar, se eliminan caracteres especiales como los acentos y se cambia el nombre de la columnas de barrio y distrito.

In [6]:
short_names = np.sort(dfi['Localización'].unique())

def unify_district(n):
    for sn in short_names:
        if sn in n:
            return sn
    
df['district'] = df['neighbourhood_group_cleansed'].apply(lambda x: unify_district(x))
df['district'] = df['district'].apply(lambda x: remove_accents(x))
dfi['Localización'] = dfi['Localización'].apply(lambda x: remove_accents(x))
df.drop(['neighbourhood_group_cleansed'], axis=1, inplace=True)

df['neighbourhood'] = df['neighbourhood_cleansed']
df['neighbourhood'] = df['neighbourhood'].apply(lambda x: remove_accents(x))
df.drop(['neighbourhood_cleansed'], axis=1, inplace=True)

Características de tipo fecha

Se realiza la conversión de formato texto a formato fecha para facilitar posteriores operaciones.

In [7]:
df['first_review'] = pd.to_datetime(df['first_review'], format='%Y-%m-%d')
df['last_review'] = pd.to_datetime(df['last_review'], format='%Y-%m-%d')

Características dinerarias

Se realiza la conversión de formato texto en dólares a formato numérico en euros.

In [8]:
dollar_to_euro_rate = 0.9

dollar_cols = [
    'price',
    'security_deposit', 
    'cleaning_fee', 
    'extra_people'
]

for col in dollar_cols:
    if col in df.columns:
        df[col].fillna('$0', inplace=True)
        df[col] = df[col].apply(lambda x: clean_price_dollar(x))
        df[col] = df[col].astype(float)
        df[col] = df[col] * dollar_to_euro_rate
        df[col] = df[col].round(2)
In [9]:
# Dataset Idealista
df_price_rent_m2 = dfi[['Localización', 'Precio m2 oct 2019']].copy()
df_price_rent_m2.columns = ['district', 'price_rent_eur_m2']
df_price_rent_m2['price_rent_eur_m2'] = df_price_rent_m2['price_rent_eur_m2'].apply(lambda x: clean_price_eur(x))
df_price_rent_m2['price_rent_eur_m2'] = df_price_rent_m2['price_rent_eur_m2'].astype(float)

Características binarias

Se realiza la conversión de formato texto (t para true, f para false) a formato numérico (0 para false, 1 para true).

In [10]:
bin_cols = [
    'host_is_superhost',
    'host_identity_verified', 
    'is_location_exact',
    'has_availability',
    'instant_bookable'
]
    
for col in bin_cols:
    if col in df.columns:
        df[col] = df[col].apply(lambda x: get_bin_value_by_char(x))
        df[col] = df[col].astype(int)

Adaptación de equipamientos

Se realiza la conversión de un formato no estructurado extrayendo del texto los tipos de equipamientos más relevantes que declara cada vivienda y creando una característica binaria para cada tipo de equipamiento. Esto además simplificará la gestión de variables categóricas en el modelado y facilitará en algunos aspectos la exploración.

In [11]:
df['amenities'] = df['amenities'].str.lower()
amenities_dict = {}

def collect_amenities(str):
    str = str.replace('{', '').replace('}', '').replace('\"', '').strip()
    word_list = str.split(",")
    for w in word_list:
        w = w.strip()
        amenities_dict[w] = amenities_dict.get(w, 0) + 1
    
df['amenities'].apply(lambda x: collect_amenities(x))
amenities_dict = sorted(amenities_dict.items(), key=lambda x: x[1], reverse=True)

top_amenities = [
    'wifi', 
    'essentials', 
    'kitchen', 
    'heating', 
    'washer', 
    'hangers', 
    'tv', 
    'hair dryer', 
    'iron', 
    'shampoo',
    'laptop friendly workspace',
    'air conditioning', 
    'hot water',
    'elevator',
    'refrigerator',
    'dishes and silverware',
    'microwave',
    'bed linens',
    'no stairs or steps to enter',
    'coffee maker',
    'cooking basics',
    'family/kid friendly',
    'long term stays allowed',
    'first aid kit',
    'oven',
    'stove'
]

for v in top_amenities:
    new_col_name = 'has_' + v.replace(' ', '_')
    df[new_col_name] = df['amenities'].apply(lambda x: contains_bin_value(v, x))
    df[new_col_name] = df[new_col_name].astype(int)
    
df.drop(['amenities'], axis=1, inplace=True)

Adaptación de las verificaciones del anfitrión

Se realiza la conversión de un formato no estructurado extrayendo del texto los tipos de verificaciones más relevantes que declara cada anfitrión y creando una característica binaria para cada tipo de verificación. Esto además simplificará la gestión de variables categóricas en el modelado y facilitará en algunos aspectos la exploración.

In [12]:
"""
df['host_verifications'] = df['host_verifications'].str.lower()
hverif_dict = {}

def collect_host_verifications(str):
    str = str.replace('[', '').replace(']', '').replace('\"', '').replace('\'', '').strip()
    word_list = str.split(",")
    for w in word_list: 
        w = w.strip()
        hverif_dict[w] = hverif_dict.get(w, 0) + 1
    
df['host_verifications'].apply(lambda x: collect_host_verifications(x))
hverif_dict = sorted(hverif_dict.items(), key=lambda x: x[1], reverse=True)

top_verification_modes = [
    'phone',
    'email',
    'government_id',
    'reviews',
    'jumio',
    'offline_government_id',
    'selfie',
    'identity_manual',
    'facebook',
    'work_email',
    'google'
]

for v in top_verification_modes:
    new_col_name = 'host_verified_by_' + v.replace(' ', '_')
    df[new_col_name] = df['host_verifications'].apply(lambda x: contains_bin_value(v, x))
    df[new_col_name] = df[new_col_name].astype(int)
    
df.drop(['host_verifications'], axis=1, inplace=True)
"""
Out[12]:
'\ndf[\'host_verifications\'] = df[\'host_verifications\'].str.lower()\nhverif_dict = {}\n\ndef collect_host_verifications(str):\n    str = str.replace(\'[\', \'\').replace(\']\', \'\').replace(\'"\', \'\').replace(\'\'\', \'\').strip()\n    word_list = str.split(",")\n    for w in word_list: \n        w = w.strip()\n        hverif_dict[w] = hverif_dict.get(w, 0) + 1\n    \ndf[\'host_verifications\'].apply(lambda x: collect_host_verifications(x))\nhverif_dict = sorted(hverif_dict.items(), key=lambda x: x[1], reverse=True)\n\ntop_verification_modes = [\n    \'phone\',\n    \'email\',\n    \'government_id\',\n    \'reviews\',\n    \'jumio\',\n    \'offline_government_id\',\n    \'selfie\',\n    \'identity_manual\',\n    \'facebook\',\n    \'work_email\',\n    \'google\'\n]\n\nfor v in top_verification_modes:\n    new_col_name = \'host_verified_by_\' + v.replace(\' \', \'_\')\n    df[new_col_name] = df[\'host_verifications\'].apply(lambda x: contains_bin_value(v, x))\n    df[new_col_name] = df[new_col_name].astype(int)\n    \ndf.drop([\'host_verifications\'], axis=1, inplace=True)\n'

Adaptación de la licencia

Lo relevante para el estudio es si tiene licencia o no, sin importar el identificador.

In [13]:
df['license'].fillna(0, inplace=True)
df['has_license'] = df['license'].apply(lambda x: 1 if x != 0 else 0)
df.drop(['license'], axis=1, inplace=True)

Nuevas características calculadas

Se van a generar una serie de características nuevas para enriquecer el estudio descriptivo.

Tiempo de actividad

Se trata del período que la vivienda lleva explotándose de forma efectiva y se estima como el tiempo transcurrido en meses desde la primera estancia hasta la última.

In [14]:
df['activity_months'] = (df['last_review'] - df['first_review']) / np.timedelta64(1, 'M')
df['activity_months'].fillna(0, inplace=True)

Ingresos por estancia y nivel de ocupación

El precio por noche (price) no es un indicador definitivo de los ingresos por estancia porque el precio de una estancia viene determinada por otros factores adicionales:

  • Nº de huéspedes (algunos están incluidos en la tarifa price (guests-included) y el resto pagan una tarifa extra por noche (extra-people)
  • Tasas de limpieza (cleaning-fee)
  • Nº de noches
  • Nº de huéspedes (accomodates) como capacidad máxima porque beds no está informado en muchos de los casos

Por ello también se puede calcular una variable que estime el coste total de una estancia media tomando como media de número de noches 4.2 y como número de huéspedes el valor medio entre el mínimo y el máximo de ocupantes de la vivienda.

Fuente: Informe de actividad económica de Airbnb en la ciudad de Barcelona

In [15]:
df['cleaning_fee'].fillna(0, inplace=True)
df['extra_people'].fillna(0, inplace=True)
df['guests_included'].fillna(0, inplace=True)
df['accommodates'].fillna(0, inplace=True)

avg_days = 4.2

df['income_med_occupation'] = df.apply(
    lambda r: calculate_income_med_occupation(
        r['price'], 
        r['cleaning_fee'], 
        r['accommodates'], 
        r['extra_people'], 
        r['guests_included'], 
        avg_days), 
    axis=1
)

df['price_med_occupation_per_accommodate'] = df.apply(
    lambda r: calculate_price_med_occupation_per_accommodate(
        r['price'], 
        r['cleaning_fee'], 
        r['accommodates'], 
        r['extra_people'], 
        r['guests_included'], 
        avg_days), 
    axis=1
)

Valores extremos (outliers)

Tipo de propiedad

Se excluyen propiedades como los hoteles que ya de por sí son negocios hosteleros ya que el estudio pretende ceñirse a viviendas.

In [16]:
outliers_idx = df[~df['property_type'].isin(['Apartment', 'House', 'Chalet', 'Condominium', 'Loft'])].index
remove_outliers(df, outliers_idx, debug_col='property_type')
1711 outliers to be removed with values: ['Aparthotel', 'Barn', 'Bed and breakfast', 'Boat', 'Boutique hotel', 'Cabin', 'Camper/RV', 'Casa particular (Cuba)', 'Castle', 'Dome house', 'Earth house', 'Farm stay', 'Guest suite', 'Guesthouse', 'Hostel', 'Hotel', 'Island', 'Nature lodge', 'Other', 'Serviced apartment', 'Tiny house', 'Townhouse', 'Villa']
In [17]:
outliers_idx = df[df['room_type'].isin(['Hotel room'])].index
remove_outliers(df, outliers_idx, debug_col='room_type')
28 outliers to be removed with values: ['Hotel room']

Viviendas sin reviews

Las viviendas sin reviews, ya que no hay ningún indicio para saber si llevan o no tiempo publicadas, se excluyen del estudio.

In [18]:
df['number_of_reviews'].fillna(0, inplace=True)
outliers_idx = df[df['number_of_reviews'] < 2]['number_of_reviews'].index.tolist()
remove_outliers(df, outliers_idx, debug_col='number_of_reviews')
5338 outliers to be removed with values: [0, 1]

Precios por huésped extremos

Se aplica arbitrariamente una regla de rango intercuartil (IQR) para excluir precios extremos.

In [19]:
outliers_idx = get_outliers_iqr(df['price_med_occupation_per_accommodate'], 4.5)[0]
remove_outliers(df, outliers_idx, debug_col='price_med_occupation_per_accommodate')
outliers between following bounds: -346.32000000000005 712.08
175 outliers to be removed with values: [720.9, 725.4, 731.7, 738.0, 751.5, 755.24, 756.0, 759.42, 760.5, 765.0, 767.7, 769.5, 772.2, 774.81, 777.6, 777.96, 778.5, 779.22, 783.0, 787.5, 795.0, 801.0, 803.07, 804.42, 805.5, 805.95, 807.75, 808.2, 812.25, 820.08, 823.5, 834.3, 839.16, 850.05, 852.0, 858.6, 875.79, 882.0, 884.25, 886.5, 901.8, 922.14, 929.4, 933.48, 944.55, 945.0, 948.6, 957.6, 960.75, 970.2, 990.0, 1005.3, 1039.32, 1062.0, 1102.23, 1106.19, 1120.5, 1161.0, 1162.89, 1296.54, 1323.0, 1431.0, 1512.0, 1602.0, 1611.0, 1627.29, 1772.64, 1795.32, 1802.88, 1888.11, 1890.0, 1899.0, 1908.0, 1912.5, 1944.81, 1984.5, 2092.5, 2094.0, 2158.02, 2646.0, 2835.0, 2846.25, 3172.5, 3776.22, 3780.0, 3807.0, 4753.12, 5670.0, 5692.5, 6300.0, 6321.0, 6322.5, 6330.0, 9450.0, 9468.0, 9472.5, 9483.75, 11340.0, 18900.0, 18945.0, 34473.6]

Número mínimo de noches extremo

Hay viviendas anunciadas cuyo mínimo de noches por estancia es tan elevado (meses, un año o varios años) que se puede deducir que su objetivo o su intención es un alquier de tipo residencial estable, no vacacional.

Se excluyen de forma arbitraria los pisos cuyo número mínimo de noches supere las 70 por considerarlo un alquiler no vacacional.

In [20]:
outliers_idx = df[df['minimum_nights_avg_ntm'] > 70].index
remove_outliers(df, outliers_idx, debug_col='minimum_nights_avg_ntm')
43 outliers to be removed with values: [72.0, 75.0, 76.6, 80.0, 83.0, 84.9, 86.0, 88.0, 89.0, 90.0, 93.0, 96.0, 99.0, 100.0, 120.0, 124.0, 150.0, 180.0, 210.0, 300.0, 360.0, 364.0, 365.0]

Número de habitaciones extremo

Se aplica arbitrariamente una regla de rango intercuartil (IQR) para excluir valores extremos.

In [21]:
df['bedrooms'].fillna(0, inplace=True)
outliers_idx = get_outliers_iqr(df['bedrooms'], 25)[0]
remove_outliers(df, outliers_idx, debug_col='bedrooms')
outliers between following bounds: -24.0 27.0
0 outliers to be removed with values: []

Análisis exploratorio

Las características que a priori son las más importantes en el problema que se plantea son los precios y las reviews, por la relación directa en los ingresos del anfitrión y en los costes para el huésped.

Las reviews constituyen la manera más cercana de estimar el nivel de ocupación de la vivienda a falta de datos explícitos al respecto como podrían ser las duraciones de las estancias o el número de huéspedes que definen cada estancia.

En los siguientes apartados se analizan diferentes relaciones entre las múltiples características de los alojamientos, siendo especialmente relevantes los precios y las reviews como se comentaba.

Distribución del precio

El precio price es la variable objetivo del estudio luego es conveniente ver de partida cómo está distribuido.

Entre 20 y 100 euros por noche se concentra la gran mayoría de alojamientos.

In [22]:
fig = px.histogram(df, x="price", nbins=40)
fig.show()
In [23]:
fig = ff.create_distplot([df['price']], ['price'], bin_size=[25])
fig.show()

Resumen del resto de características

In [24]:
pandas_profiling.ProfileReport(df)
Out[24]:

Correlaciones

En los mapas de correlación se pueden obtener algunos indicios interesantes de relaciones entre características a parte de las relaciones evidentes por familias como por ejemplo la familia de los precios o la familia de las reviews.

In [25]:
def print_corr_map(df, height=None):
    corrs = df.corr()
    fig = go.Figure(
        data=go.Heatmap(
            z=corrs.values,
            x=list(corrs.columns),
            y=list(corrs.index),
            showscale=True
        )
    )
    
    if height:
        fig.update_layout(height=height)
        
    fig.show()
In [26]:
key_cols = [
    'price',
    'price_med_occupation_per_accommodate',
    'income_med_occupation',
    'review_scores_rating',
    'reviews_per_month'
]
In [27]:
misc_cols = [
    'activity_months',
    'accommodates',
    'bathrooms', 
    'bedrooms',
    'cancellation_policy', 
    'cleaning_fee',
    'district',
    'extra_people',
    'first_review',
    'guests_included',
    'instant_bookable',
    'has_license',
    'host_response_time',
    'latitude',
    'longitude',
    'maximum_nights_avg_ntm',
    'minimum_nights_avg_ntm',
    'neighbourhood',
    'number_of_reviews',
    'number_of_reviews_ltm',
    'property_type',
    'room_type',
    'security_deposit',
    *key_cols
]

print_corr_map(df[misc_cols], height=900)
In [28]:
review_cols = [
    'activity_months',
    'instant_bookable',
    'review_scores_accuracy',
    'review_scores_cleanliness',
    'review_scores_checkin',
    'review_scores_communication',
    'review_scores_location',
    'review_scores_value',
    *key_cols
]

print_corr_map(df[review_cols])

Licencia

La mayoría de las viviendas presentan la licencia turística.

In [29]:
df_by_license = df.groupby(['has_license'])['has_license'].count().to_frame('has_license_count')
df_by_license.reset_index(inplace=True)

fig32 = go.Figure(
        go.Pie(
            labels=['no_license', 'has_license'], 
            values=df_by_license['has_license_count']
        )
)

fig32.update_traces(
    textfont_size=20,
    marker=dict(colors=['Orange', 'SteelBlue'])
)

fig32.update_layout(title='license')
fig32.show()

fig322 = go.Figure()
for val in [0, 1]:
    fig322.add_trace(
        go.Violin(
            x=df['has_license'][df['has_license'] == val],
            y=df['reviews_per_month'][df['has_license'] == val],
            name=val,                
            meanline_visible=True,
            line_color='Orange' if val == 0 else 'SteelBlue'
        )
    )

fig322.update_layout(title='reviews_per_month x has_license', showlegend=False)
fig322.show()

Tipos de propiedad y relación con precio por huésped

Claro dominio de apartamentos. En el precio no hay diferencias notables.

In [30]:
df_by_property_type = df.groupby(['property_type'])['property_type'].count().to_frame('property_type_count')
df_by_property_type.reset_index(inplace=True)
property_types = np.sort(df_by_property_type['property_type'].unique())

fig33 = go.Figure(go.Pie(
    labels=df_by_property_type['property_type'], 
    values=df_by_property_type['property_type_count']
))

fig33.update_layout(title='property_type')
fig33.update_traces(textfont_size=15)
fig33.show()

fig332 = go.Figure()
for pt in property_types:
    fig332.add_trace(
        go.Violin(
            x=df['property_type'][df['property_type'] == pt],
            y=df['price_med_occupation_per_accommodate'][df['property_type'] == pt],
            points='all',
            name=pt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig332.update_layout(title='price_med_occupation_per_accommodate')
fig332.show()

Tipos de habitaciones y relación con precio por huésped

El formato más habitual de alquiler vacacional es la habitación privada seguido del piso completo. Los precios son sólo ligeramente superiores en los pisos completos respecto a la habitación privada.

In [31]:
df_by_room_type = df.groupby(['room_type'])['room_type'].count().to_frame('room_type_count')
df_by_room_type.reset_index(inplace=True)
room_types = np.sort(df_by_room_type['room_type'].unique())

fig34 = go.Figure(
        go.Pie(
            labels=df_by_room_type['room_type'], 
            values=df_by_room_type['room_type_count']
        )
)

fig34.update_traces(textfont_size=20)
fig34.update_layout(title='room_type')
fig34.show()

fig342 = go.Figure()
for rt in room_types:
    fig342.add_trace(
        go.Violin(
            x=df['room_type'][df['room_type'] == rt],
            y=df['price_med_occupation_per_accommodate'][df['room_type'] == rt],
            points='all',
            name=rt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig342.update_layout(title='price_med_occupation_per_accommodate')
fig342.show()

Relación entre número y puntuación de las reviews

Puede haber una cierta retroalimentación entre el número de reviews por mes y la nota en las mismas. Lo normal es que cuantas más y mejores reviews tenga una vivienda, más estancias genere.

In [57]:
fig270 = px.scatter(
    df, 
    x='review_scores_rating', 
    y='reviews_per_month', 
    color='room_type'
)

fig270.show()

Relación entre política de cancelación y número de reviews mensuales

No parece que haya un claro impacto del tipo de cancelación sobre el número de estancias. En general los anfitriones utilizan políticas de cancelación que tienen un mínimo de flexibilidad en el tiempo para no ahuyentar a potenciales huéspedes.

In [33]:
df_by_cancellation_policy = df.groupby(['cancellation_policy'])['cancellation_policy'].count().to_frame('cancellation_policy_count')
df_by_cancellation_policy.reset_index(inplace=True)
cancellation_policy_types = np.sort(df_by_cancellation_policy['cancellation_policy'].unique())

fig36 = go.Figure(go.Pie(
    labels=df_by_cancellation_policy['cancellation_policy'], 
    values=df_by_cancellation_policy['cancellation_policy_count']
))

fig36.update_layout(title='cancellation_policy')
fig36.update_traces(textfont_size=15)
fig36.show()

fig362 = go.Figure()
for cpt in cancellation_policy_types:
    fig362.add_trace(
        go.Violin(
            x=df['cancellation_policy'][df['cancellation_policy'] == cpt],
            y=df['reviews_per_month'][df['cancellation_policy'] == cpt],
            name=cpt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig362.update_layout(title='reviews_per_month', showlegend=False)
fig362.show()

Relación entre número mínimo de noches y número de reviews mensuales

El número mínimo de noches a la hora de reservar un alojamiento podría ser un factor limitante. Simplemente es un indicio porque no se conoce la duración de las estancias pero los alojamientos que menos limitan este aspecto tienen más estancias al mes.

In [58]:
fig290 = px.scatter(
    df, 
    x='minimum_nights_avg_ntm', 
    y='reviews_per_month'
)

fig290.show()

Relación entre tiempo de respuesta y número de reviews mensuales

A mejor tasa de respuesta, más estancias. Es importante por tanto la agilidad de los anfitriones a la hora de tratar con los potenciales huéspedes.

In [35]:
df['host_response_time'].fillna('-unk-', inplace=True)
df_by_response_time = df.groupby(['host_response_time'])['host_response_time'].count().to_frame('host_response_time_count')
df_by_response_time.reset_index(inplace=True)
response_time_types = np.sort(df_by_response_time['host_response_time'].unique())

fig38 = go.Figure()
for rtt in response_time_types:
    fig38.add_trace(
        go.Violin(
            x=df['host_response_time'][df['host_response_time'] == rtt],
            y=df['reviews_per_month'][df['host_response_time'] == rtt],
            name=rtt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig38.update_layout(title='reviews_per_month', showlegend=False)
fig38.show()

Tasa de limpieza

Una mayor tasa de limpieza se relaciona con viviendas que se alquilan de forma completa. Cuanto más alta es la tasa de limpieza, menos reservas al mes se obtienen posiblemente por tratarse de viviendas exclusivas.

In [59]:
fig211 = px.scatter(
    df, 
    x='cleaning_fee', 
    y='reviews_per_month', 
    color='room_type'
)

fig211.show()
In [60]:
fig2112 = px.scatter(
    df, x='cleaning_fee', 
    y='income_med_occupation', 
    color='room_type'
)

fig2112.show()

Número de alojamientos por barrio

El centro tiene una concentración mucho más alta de alojamientos.

In [61]:
df_by_nb = df.groupby(['neighbourhood'])['neighbourhood'].size().to_frame('count')
df_by_nb.reset_index(inplace=True)

fig310 = go.Figure(go.Choroplethmapbox(
    geojson=city_nb,
    locations=df_by_nb['neighbourhood'], 
    z=df_by_nb['count'],                   
    colorscale='Blues',                                
    marker_opacity=0.5, 
    marker_line_width=0.5
))

fig310.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig310.show()

Relación entre precio por húesped y localización

En general son los barrios del centro y del litoral los que presentan precios un poquito más altos.

In [62]:
fig311 = go.Figure(
    go.Scattermapbox(
        lon=df['longitude'],
        lat=df['latitude'],
        mode='markers',
        marker_color=df['price_med_occupation_per_accommodate'],
        text=df['price_med_occupation_per_accommodate'],
        marker=dict(
            opacity=0.5, 
            colorscale='Blues',
            cmin=df['price'].min(), 
            cmax=df['price'].max()
        )
    )
)

fig311.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig311.show()

Relación entre precio por huésped y notas de review

Las notas de las reviews no tienen una relación clara con el precio pagado por el huésped (ya sea alto o bajo).

In [63]:
px.scatter(
    df, 
    x='review_scores_rating',
    y='price_med_occupation_per_accommodate'
).show()

Relación entre precio por huésped y número de reviews mensuales

Los alojamientos con precios por noche en el rango entre 130 y 230 euros son los que consiguen más estancias.

In [64]:
px.scatter(
    df,
    x='price_med_occupation_per_accommodate',
    y='reviews_per_month',
    marginal_x='box', 
    trendline='ols'
).show()

Review de localización

La valoración de localización es un aspecto muy abierto e interesante. Se puede considerar una localización en función de si está bien conectada con el resto de la ciudad, o de si está próxima a puntos de interés, o de si está bien ubicado con respecto a cualquier intención concreta ya sea de tipo turístico, laboral, familiar, etc. que cualquier huésped puede tener.

En el mapa se muestra que efectivamente los barrios del centro tienen una gran calificación en este aspecto pero tampoco hay diferencias muy grandes con barrios un poco más lejanos al centro.

In [65]:
df_by_nb = df.groupby(['neighbourhood'])['review_scores_location'].mean().to_frame('review_scores_location_avg')
df_by_nb.reset_index(inplace=True)

fig3142 = go.Figure(go.Choroplethmapbox(
    geojson=city_nb,
    locations=df_by_nb['neighbourhood'], 
    z=df_by_nb['review_scores_location_avg'],                   
    colorscale='Blues',                                
    marker_opacity=0.5, 
    marker_line_width=0.5
))

fig3142.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig3142.show()

Distribución del precio por huésped

Entre 60 y 300 euros por noche es el rango más habitual de precio que paga un huésped por noche por una estancia de duración media.

In [43]:
fig = px.histogram(df, x='price_med_occupation_per_accommodate', nbins=40)
fig.show()
In [44]:
fig = go.Figure()

fig.add_trace(go.Histogram(
    x=df['price_med_occupation_per_accommodate'],
    name='price_med_occupation_per_accommodate',
    marker_color='#EB89B5'))

fig.add_trace(go.Histogram(
    x=df['price'],
    name='price',
    marker_color='#330C73'))

"""
fig.add_trace(go.Histogram(
    x=df['income_med_occupation'],
    name='income_med_occupation',
    marker_color='green'))
"""

fig.update_layout(barmode='stack')
fig.update_traces(opacity=0.75)
fig.show()

Relación entre habitaciones y baños

Dominan con claridad los alojamientos con pocas estancias, en concreto con un baño y un dormitorio.

In [45]:
plt.figure(figsize=(16, 8))
plt.hist(df['bedrooms'], bins=15)
plt.gca().set(title='BEDROOMS', ylabel='COUNT');
In [46]:
df['bathrooms'].fillna(0, inplace=True)
plt.figure(figsize=(16, 8))
plt.hist(df['bathrooms'], bins=15)
plt.gca().set(title='BATHROOMS', ylabel='COUNT');
In [47]:
df_bb = df.groupby(['bedrooms', 'bathrooms'])[['bedrooms', 'bathrooms']].size().to_frame('cnt')
df_bb.reset_index(inplace=True)
fig316 = px.scatter(df_bb, x='bedrooms', y='bathrooms', size='cnt')
fig316.show()

Relación entre ingresos por estancia y capacidad

In [48]:
df_by_price_per_bed = df.groupby(['district', 'accommodates'])['income_med_occupation'].mean().to_frame('income_med_occupation_avg')
df_by_price_per_bed.reset_index(inplace=True)

px.line(
    df_by_price_per_bed, 
    x='accommodates', 
    y='income_med_occupation_avg',
    color='district'
).show()

Distribución de ingresos por estancia según el distrito

In [49]:
districts = np.sort(df['district'].unique())[::-1]      
    
fig318 = go.Figure()
for d in districts:
    fig318.add_trace(go.Violin(
        x=df[df['district'] == d]['income_med_occupation'].values,
        name='  ' + d + '  '
    ))

fig318.update_traces(
    orientation='h', 
    side='positive', 
    width=3, 
    points=False
)

fig318.update_layout(
    height=900,
    xaxis_showgrid=False, 
    xaxis_zeroline=False, 
    showlegend=False
)

fig318.show()
In [66]:
px.scatter(
    df,
    x='accommodates',
    y='income_med_occupation', 
    trendline='ols',
    color='district'
).show()

Relación entre barrio y precio por huésped

Son en general los barrios del centro y del litoral los que mayor precio por huésped tienen y los del norte los que menor aunque hay algunas excepciones.

In [67]:
df_by_nb = df.groupby(['neighbourhood'])['price_med_occupation_per_accommodate'].mean().to_frame('price_med_occupation_per_accommodate_avg')
df_by_nb.reset_index(inplace=True)

fig320 = go.Figure(go.Choroplethmapbox(
    geojson=city_nb,
    locations=df_by_nb['neighbourhood'], 
    z=df_by_nb['price_med_occupation_per_accommodate_avg'],                   
    colorscale='Blues',                                
    marker_opacity=0.5, 
    marker_line_width=0.5
))

fig320.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig320.show()

Relación entre precio por huésped y precio medio de alquiler por m2

Los precios por huésped están bastante alineados con el precio de alquiler que presenta el mercado inmobiliario.

In [68]:
df_by_district = df.groupby(['district'])['price_med_occupation_per_accommodate'].mean().to_frame('price_med_occupation_per_accommodate_avg')
df_by_district.reset_index(inplace=True)
df_by_district = df_by_district.merge(df_price_rent_m2) # join

fig322 = px.scatter(
    df_by_district, 
    x='price_rent_eur_m2',
    y='price_med_occupation_per_accommodate_avg'
)

fig322.show()

Relación entre precio por huésped e ingresos por estancia media

A lo largo del estudio se han presentado diferentes relaciones entre características donde participan el precio medio por noche y huésped y los ingresos por estancia media. Ambas variables están basadas en la característica precio y por ello tienen una clara relación con ella así que es probable que en la fase de modelado se descarte el uso de alguna de ellas.

In [69]:
fig = go.Figure()

fig.add_trace(go.Histogram(
    x=df['price_med_occupation_per_accommodate'],
    name='price_med_occupation_per_accommodate',
    marker_color='#EB89B5'))

fig.add_trace(go.Histogram(
    x=df['income_med_occupation'],
    name='income_med_occupation',
    marker_color='blue'))

fig.update_layout(barmode='stack')
fig.update_traces(opacity=0.75)
fig.show()
In [70]:
fig323 = go.Figure()
for x in ['price_med_occupation_per_accommodate', 'income_med_occupation']:
    fig323.add_trace(go.Scatter(
        x=df['price'], 
        y=df[x], 
        mode='markers',
        name=x,
        marker=dict(color=('blue' if x != 'price_med_occupation_per_accommodate' else '#EB89B5'))
    ))
    fig323.update_traces(marker=dict(opacity=0.5))

fig323.show()

Equipamiento de la vivienda

En las correlaciones simplemente se observa más afinidad entre objetos que normalmente se ubican en una cocina como el frigorífico, el microondas, la cafetera o la vajilla.

No existe un impacto claro entre alguna comodidad concreta y las reviews en número o nota de las estancias.

Curiosamente el aire acondicionado es una de las comodidades que menos destaca, bien es cierto que es algo que tiene importancia sobre todo de forma estacional y según para qué tipo de huéspedes.

In [71]:
reviews_cols = [
    'reviews_per_month', 
    'review_scores_rating'
]

amenities_cols = [
    'has_air_conditioning',
    'has_bed_linens',
    'has_coffee_maker',
    'has_cooking_basics',
    'has_dishes_and_silverware',
    'has_elevator',
    'has_essentials', 
    'has_family/kid_friendly',
    'has_first_aid_kit',
    'has_hair_dryer',
    'has_hangers',
    'has_heating',
    'has_hot_water',
    'has_iron',
    'has_kitchen',
    'has_laptop_friendly_workspace',
    'has_microwave',
    'has_no_stairs_or_steps_to_enter',
    'has_oven',
    'has_refrigerator',
    'has_shampoo',
    'has_stove',
    'has_tv',
    'has_washer',
    'has_wifi'
]

print_corr_map(df[[*amenities_cols, *reviews_cols]], height=900)

"""
for am in amenities_cols:
    fig324 = go.Figure()
    fig324 = make_subplots(
        rows=1, 
        cols=2, 
        subplot_titles=('reviews_per_month', 'review_scores_rating')
    )
    
    for var in reviews_cols:
        for val in [0, 1]:
            fig324.add_trace(go.Violin(
                    x=df[am][df[am] == val],
                    y=df[var][df[am] == val],
                    name=val,                
                    meanline_visible=True,
                    line_color='Orange' if val == 0 else 'SteelBlue'
                ),
                row=1, 
                col=1 if var == 'reviews_per_month' else 2
            )

        fig324.update_layout(title=am, showlegend=False)
        
    fig324.show()
"""
Out[71]:
"\nfor am in amenities_cols:\n    fig324 = go.Figure()\n    fig324 = make_subplots(\n        rows=1, \n        cols=2, \n        subplot_titles=('reviews_per_month', 'review_scores_rating')\n    )\n    \n    for var in reviews_cols:\n        for val in [0, 1]:\n            fig324.add_trace(go.Violin(\n                    x=df[am][df[am] == val],\n                    y=df[var][df[am] == val],\n                    name=val,                \n                    meanline_visible=True,\n                    line_color='Orange' if val == 0 else 'SteelBlue'\n                ),\n                row=1, \n                col=1 if var == 'reviews_per_month' else 2\n            )\n\n        fig324.update_layout(title=am, showlegend=False)\n        \n    fig324.show()\n"

Exportación dataset

In [72]:
df.to_csv(filename_out, index=False)